<!DOCTYPE html>
<!--
Take frames coming from various sources and read them using copyTo().
-->
<title>copyTo() test</title>
<script src="webcodecs_common.js"></script>
<script type="text/javascript">
  'use strict';

  function yuv_to_rgb601(y, u, v) {
    let b = 1.164 * (y - 16) + 2.018 * (u - 128);
    let g = 1.164 * (y - 16) - 0.813 * (v - 128) - 0.391 * (u - 128);
    let r = 1.164 * (y - 16) + 1.596 * (v - 128);
    return { r: r, g: g, b: b };
  }

  function yuv_to_rgb709(y, u, v) {
    let b = 1.164 * (y - 16) + 2.113 * (u - 128);
    let g = 1.164 * (y - 16) - 0.533 * (v - 128) - 0.213 * (u - 128);
    let r = 1.164 * (y - 16) + 1.793 * (v - 128);
    return { r: r, g: g, b: b };
  }

  async function validateFourColorsBytes(frame) {
    const m = 4;
    const tolerance = 8;
    let expected_xy_color = [
      // Left-top yellow
      { x: m, y: m, r: 255, g: 255, b: 0 },
      // Right-top red
      { x: frame.displayWidth - m, y: m, r: 255, g: 0, b: 0 },
      // Left-bottom blue
      { x: m, y: frame.displayHeight - m, r: 0, g: 0, b: 255 },
      // Right-bottom green
      { x: frame.displayWidth - m, y: frame.displayHeight - m,
        r: 0, g: 255, b: 0 },
    ]

    for (let test of expected_xy_color) {
      const options = {
        rect: { x: test.x, y: test.y, width: 2, height: 2 }
      }
      let size = frame.allocationSize(options);
      let buffer = new ArrayBuffer(size);
      let layout = await frame.copyTo(buffer, options);
      let view = new DataView(buffer);
      let rgb = null;

      let yuv_to_rgb = yuv_to_rgb601;
      if (frame.colorSpace.matrix == "bt709")
        yuv_to_rgb = yuv_to_rgb709;

      switch (frame.format) {
        case "I420":
        case "I420A":
          rgb = yuv_to_rgb(view.getUint8(layout[0].offset),   // Y
            view.getUint8(layout[1].offset),                  // U
            view.getUint8(layout[2].offset));                 // V
          break;
        case "NV12":
          rgb = yuv_to_rgb(view.getUint8(layout[0].offset),   // Y
            view.getUint8(layout[1].offset),                  // U
            view.getUint8(layout[1].offset + 1));             // V
          break;
        case "RGBX":
        case "RGBA":
          rgb = {
            r: view.getUint8(layout[0].offset),
            g: view.getUint8(layout[0].offset + 1),
            b: view.getUint8(layout[0].offset + 2)
          };
          break;
        case "BGRX":
        case "BGRA":
          rgb = {
            r: view.getUint8(layout[0].offset + 2),
            g: view.getUint8(layout[0].offset + 1),
            b: view.getUint8(layout[0].offset)
          };
          break;
        default:
          TEST.reportFailure("Unexpected frame format: " + frame.format);
          return;
      }

      let message =
          `Test ${frame.format} ${JSON.stringify(test)} ${JSON.stringify(rgb)}`;
      TEST.assert(Math.abs(rgb.r - test.r) < tolerance, message);
      TEST.assert(Math.abs(rgb.g - test.g) < tolerance, message);
      TEST.assert(Math.abs(rgb.b - test.b) < tolerance, message);
    }
  }

  function readWholeBuffer(buffer) {
    let bytes = new Uint8Array(buffer);
    return bytes.reduce((acc, byte) => { return byte ? acc + 1 : acc; }, 0);
  }

  function makeWorker() {
    const worker = new Worker('buffer-read-and-reply-worker.js');
    let resolve_promise = null;
    worker.onmessage = function(e) { resolve_promise(e.data); }
    let worker_promise = new Promise((resolve) => {
      resolve_promise = resolve;
    });
    return { worker , worker_promise };
  }

  async function main(arg) {
    let source_type = arg.source_type;
    TEST.log('Starting test with arguments: ' + JSON.stringify(arg));
    let source = await createFrameSource(source_type, 320, 240);
    if (!source) {
      TEST.skip('Unsupported source: ' + source_type);
      return;
    }

    let frame = await source.getNextFrame();
    let size = frame.allocationSize();

    // Readback a whole frame to a regular buffer detach it
    {
      let buf = new ArrayBuffer(size);
      TEST.assert(readWholeBuffer(buf) == 0, "Buffer should be zero");
      let copy_promise = frame.copyTo(buf);
      buf.transfer(1);
      let layout = await copy_promise;
      TEST.assert(layout, "layout is empty / ArrayBuffer");
    }

    // Readback a whole frame to a regular buffer and send it to a worker
    {
      let {worker, worker_promise } = makeWorker();
      let buf = new ArrayBuffer(size);
      TEST.assert(readWholeBuffer(buf) == 0, "Buffer should be zero");
      let copy_promise = frame.copyTo(buf);
      worker.postMessage(buf, [buf]);
      let result_buf = await worker_promise;
      let layout = await copy_promise;
      TEST.assert(readWholeBuffer(result_buf) > 0, "Buffer shouldn't be zero");
      TEST.assert(layout, "layout is empty / ArrayBuffer");
      worker.terminate();
    }

    // Readback a whole frame to a shared buffer and send it to a worker
    {
      let {worker, worker_promise} = makeWorker();
      let buf = new SharedArrayBuffer(size);
      TEST.assert(readWholeBuffer(buf) == 0, "Buffer should be zero");
      let copy_promise = frame.copyTo(buf);
      worker.postMessage(buf);
      await worker_promise;
      let layout = await copy_promise;
      TEST.assert(readWholeBuffer(buf) > 0, "SharedBuffer shouldn't be zero");
      TEST.assert(layout, "layout is empty / SharedArrayBuffer");
      worker.terminate();
    }

    // Validate pixels
    if (!arg.validate_camera_frames && source_type == 'camera') {
      TEST.log("Skip copyTo result validation");
    } else {
      await validateFourColorsBytes(frame);
    }

    frame.close();
    source.close();
    TEST.log('Test completed');
  }
  addManualTestButton([{'source_type': 'offscreen'}]);
  addManualTestButton([{'source_type': 'arraybuffer'}]);
</script>